package il.ac.tau.cg.ex3.entities.scene;

import il.ac.tau.cg.ex3.entities.Entity;
import il.ac.tau.cg.ex3.entities.scene.lights.Light;
import il.ac.tau.cg.ex3.entities.scene.lights.LightEntity;
import il.ac.tau.cg.ex3.entities.scene.lights.LightGroup;
import il.ac.tau.cg.ex3.entities.styles.Color;
import il.ac.tau.cg.ex3.entities.styles.Material;
import il.ac.tau.cg.ex3.entities.styles.drawables.Drawable;
import il.ac.tau.cg.ex3.entities.surfaces.Surface;
import il.ac.tau.cg.ex3.math.Params;
import il.ac.tau.cg.ex3.math.Point;
import il.ac.tau.cg.ex3.math.Vector;
import il.ac.tau.cg.ex3.math.intersectors.IntersectionResult;
import il.ac.tau.cg.ex3.math.intersectors.Intersector;
import il.ac.tau.cg.ex3.math.intersectors.OctreeIntersector;
import il.ac.tau.cg.ex3.math.intersectors.SimpleIntersector;
import il.ac.tau.cg.ex3.utils.octree.BoundingBox;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.SortedSet;

public class Scene implements Entity
{
	private static final int	MAX_RECURSION_LEVELS	= 3;
	
	private int width;
	private int height;
	
	private Color    backColor;
	private Drawable backDrawable;
	private Color    ambientColor;
	private int      superSampleWidth;
	private boolean  isAccelerated;
	
	private Camera camera;
	private List<LightEntity> lights;
	private List<Light> flatLights;
	private List<Surface> surfaces;

	private Intersector intersector;
	
	public Scene()
	{
		this(null, null, null, -1, false);
	}
	
	public Scene(Color backColor, Drawable backDrawable, Color ambientColor, int superSampleWidth, boolean isAccelerated)
	{
		this.backColor		  = backColor;
		this.backDrawable     = backDrawable;
		this.ambientColor     = ambientColor;
		this.superSampleWidth = superSampleWidth;
		this.isAccelerated    = isAccelerated;
		
		lights     = new ArrayList<LightEntity>();
		flatLights = new ArrayList<Light>();
		surfaces   = new ArrayList<Surface>();
		
		intersector = null;
	}
	
	public void finalizeInitialization()
	{
		intersector = createIntersector();
	}
	
	public Color getColor(int x, int y)
	{
		double viewPlaneWidth  = camera.getViewPlaneWidth();
		double viewPlaneHeight = height * viewPlaneWidth / width;
		
		Point lookAt = camera.getLookAt();
		
		Vector leftDirection = camera.getUpDirection().cross(camera.getViewDirection()).normalize();
		Vector upDirection   = camera.getUpDirection();
		
		Point topMidPoint  = lookAt.add(upDirection.scale(viewPlaneHeight / 2));
		Point topLeftPoint = topMidPoint.add(leftDirection.scale(viewPlaneWidth / 2));
		
		Color res = new Color(0.0, 0.0, 0.0);
		
		double widthDelta = 1.0 / (superSampleWidth + 1);
		double superX = x;
		double superY = y;
		
		for (int j = 0; j < superSampleWidth; j++)
		{
			superX += widthDelta;
			superY  = y;
			
			for (int i = 0; i < superSampleWidth; i++)
			{
				superY += widthDelta;
				
				Point worldPoint = topLeftPoint.add(leftDirection.scale((superX / width) * -viewPlaneWidth));
				worldPoint = worldPoint.add(upDirection.scale((superY / height) * -viewPlaneHeight));
				
				Vector ray = new Vector(camera.getPosition(), worldPoint).normalize();
				Color col = traceRay(camera.getPosition(), ray, 0);
				
				if (col == null)
				{
					col = backDrawable.sample(superX / width, superY / height);
				}
				
				col = col.scale(1.0 / (superSampleWidth * superSampleWidth));
				
				res = res.add(col);
			}
		}
		
		return res;
	}
	
	private Intersector createIntersector()
	{
		if (isAccelerated)
		{
			double minX = Double.POSITIVE_INFINITY;
			double maxX = Double.NEGATIVE_INFINITY;
			double minY = Double.POSITIVE_INFINITY;
			double maxY = Double.NEGATIVE_INFINITY;
			double minZ = Double.POSITIVE_INFINITY;
			double maxZ = Double.NEGATIVE_INFINITY;
			
			for (Surface surface : surfaces)
			{
				BoundingBox surfaceBox = surface.getShape().getBoundingBox();
				
				minX = Math.min(minX, surfaceBox.getMinX());
				maxX = Math.max(maxX, surfaceBox.getMaxX());
				minY = Math.min(minY, surfaceBox.getMinY());
				maxY = Math.max(maxY, surfaceBox.getMaxY());
				minZ = Math.min(minZ, surfaceBox.getMinZ());
				maxZ = Math.max(maxZ, surfaceBox.getMaxZ());
			}
			
			BoundingBox resultBox = new BoundingBox(minX, maxX, minY, maxY, minZ, maxZ);
			OctreeIntersector shapeTree = new OctreeIntersector(resultBox);
			
			shapeTree.insert(surfaces);
			
			return shapeTree;
		}
		else
		{
			return new SimpleIntersector(surfaces);
		}
	}
	private Color traceRay(Point origin, Vector direction, int level)
	{
		IntersectionResult intResult = intersector.intersect(origin, direction);
		
		if (intResult == null)
		{
			return null;
		}
		
		Surface closestSurface = (Surface)intResult.getIntersectable();
		double closestParam = intResult.getParam();
		
		Material material = closestSurface.getMaterial();
		
		Point intPoint   = origin.add(direction.scale(closestParam));
		Vector intNormal = closestSurface.getShape().getNormal(intPoint);
		
		Color emissionColor   = material.getEmission();
		Color ambientCoef     = material.getAmbient();
		Color specularCoef    = material.getSpecular();
		double shininessPower = material.getShininess();
		
		Params param = closestSurface.getShape().getParams(intPoint);
		Color diffuseCoef = material.getDrawable().sample(param.getU(), param.getV());
		
		Color result = emissionColor.add(ambientColor.mul(ambientCoef)); 
		
		for (Light light : flatLights)
		{
			Vector l = light.getDirection(intPoint).negate().normalize();
			
			if (isRayBlocked(closestSurface, closestParam, intPoint, l))
			{
				continue;
			}
			
			Color i   = light.getIntensity(intPoint);
			double nl = Math.max(intNormal.dot(l), 0.0);
			
			Color dc = diffuseCoef.scale(nl);
			
			Vector r = l.add(intNormal.scale(nl).subtract(l).scale(2.0));
			Vector v = new Vector(intPoint, camera.getPosition()).normalize();
			double vrn = Math.pow(Math.max(v.dot(r), 0.0), shininessPower);
			
			Color sc = specularCoef.scale(vrn);
			
			result = result.add(dc.add(sc).mul(i));
		}
		
		if (level < MAX_RECURSION_LEVELS)
		{
			if (material.getReflectance() > 0.0)
			{
				Vector dn = direction.negate();
				double nd = Math.max(intNormal.dot(dn), 0.0);
				Vector r  = dn.add(intNormal.scale(nd).subtract(dn).scale(2.0));
				
				Color reflectionColor = traceRay(intPoint, r, level + 1);
				
				if (reflectionColor == null)
				{
					reflectionColor = backColor.clone();
				}
				
				reflectionColor = reflectionColor.scale(material.getReflectance());
				
				result = result.add(reflectionColor);
			}
			
			//TODO: if Kt > 0
//			Color refractionColor = traceRay(intPoint, direction, level + 1);
//			
//			if (refractionColor == null)
//			{
//				refractionColor = backColor.clone();
//			}
//			
//			refractionColor = refractionColor.scale(1.0);
//			
//			result = result.add(refractionColor);
		}
		
		return result.trim();
	}
	
	public void addLight(LightEntity light)
	{
		lights.add(light);
		
		if (light instanceof LightGroup)
		{
			LightGroup lg = (LightGroup)light;
			
			for (Iterator<Light> iter = lg.iterator(); iter.hasNext(); )
			{
				flatLights.add(iter.next());
			}
		}
		else
		{
			flatLights.add((Light)light);
		}
	}
	
	public void addSurface(Surface surface)
	{
		surfaces.add(surface);
	}

	public void setCamera(Camera camera)
	{
		this.camera = camera;
	}
	
	public void setBackColor(Color backColor)
	{
		this.backColor = backColor;
	}
	
	public void setBackDrawable(Drawable backDrawable)
	{
		this.backDrawable = backDrawable;
	}

	public void setAmbientColor(Color ambientColor)
	{
		this.ambientColor = ambientColor;
	}

	public void setSuperSampleWidth(int superSampleWidth)
	{
		this.superSampleWidth = superSampleWidth;
	}
	
	public boolean isAccelerated()
	{
		return isAccelerated;
	}
	
	public void setAccelerated(boolean isAccelerated)
	{
		this.isAccelerated = isAccelerated;
	}

	public void setCanvasSize(int height, int width)
	{
		this.width  = width;
		this.height = height;
	}
	
	private boolean isRayBlocked(Surface curSurface, double curParam, Point origin, Vector direction)
	{
		for (Surface surface: surfaces)
		{
			if (surface == curSurface)
			{
				continue;
			}
			
			SortedSet<Double> ints = surface.getShape().intersect(origin, direction);
			
			if (!ints.isEmpty())
			{
				return true;
			}
		}
		
		return false;
	}
}

